// raboot of atari 2600 Boxing
// TODO: death animation (drift, arms flail)
// TODO: ropes (and some padding)
// TODO: gamepad controls
// TODO: touch controls
// TODO: finish music
// TODO: menu - 1/2 players, audio levels
// TODO: menu - gore level (in case become olympic sport),
//
// TODO: menu - hoboplayer, with broadcast discovery
//
// TODO: zombie mode - if keep punching after win, go again till 200 damage
//
// from game fade away to main menu if no input, form main menu fade away to program.end() if no input

/*
#canvas {
    position: absolute;
    top: 0px;
    left: 0px;
    margin: 0px;
    width: 100%;
    height: 100%;
    overflow: hidden;
    display: block;
}
*/

#include "common/sketchbook.hpp"
#include "common/math.hpp"

float quadratic_out(float x) { return 1.f - motion::quadratic_curve(1.f - x); };
float stairwave(float x)
{
	// TODO: must be a way to just parameterize sinus to do less work and be less smooth, instead of doing more work to make it jagged
	constexpr auto levels = 12.f;
	return int(std::clamp(common::sinus(x)  *levels, -levels,levels)) /levels;
}

struct key_vector
{
	scancode k;
	float2 v;
};

struct keymap
{
	std::array<key_vector,4> legwork;
	std::array<scancode,3> punch;
};

keymap wasd
{
	{
		key_vector{scancode::w, -float2::j()},
		key_vector{scancode::a, -float2::i()},
		key_vector{scancode::s,  float2::j()},
		key_vector{scancode::d,  float2::i()}
	},
	{ scancode::tab, scancode::f, scancode::v }
};

keymap ijkl
{
	{
		key_vector{scancode::i, -float2::j()},
		key_vector{scancode::j, -float2::i()},
		key_vector{scancode::k,  float2::j()},
		key_vector{scancode::l,  float2::i()}
	},
	{ scancode::h, scancode::semicolon, scancode::n}
};

keymap arrow_keys
{
	{
		key_vector{scancode::up,   -float2::j()},
		key_vector{scancode::left, -float2::i()},
		key_vector{scancode::down,  float2::j()},
		key_vector{scancode::right, float2::i()}
	},
	{ scancode::rctrl, scancode::rshift, scancode::enter }
};

std::vector control_options {wasd, ijkl, arrow_keys}; // FIXME: numpad, gamepad, touchscreen, mouse

rect ring{};

constexpr float2 half = float2::one(0.5);

constexpr float2 starting_pos = float2::one(0.1);

constexpr auto scale(rect ratio, rect world)
{
	ratio.size *= world.size;
	ratio.position *= world.size;
	ratio.position += world.position;
	return ratio;
}

constexpr auto faded_wave(float fade_factor, auto&& f)
{
	return [fade_factor, f = std::forward<decltype(f)>(f)](auto ratio) mutable
	{
		auto [freq, ampl] = f(ratio);
		float fade = std::min(ratio*fade_factor, (1-ratio)*fade_factor);
		return way(0.f,common::sinus(ratio * freq) * ampl, std::min(fade, 1.f));
	};
};


struct blood_drop
{
	rgb cola;
	movement<float2, motion::cubic_curve> veloc;
	float2 position;
	float size = 0.01f;

	void update(frame& frame, auto delta)
	{
		veloc.advance(delta);
		auto oldpos = position;
		position += veloc.value();

		frame.begin_sketch()
			.line(ring.size*oldpos + ring.position, ring.size*position + ring.position)
			.line_width(size * ring.size.x())
			.line_cap(sketch::cap::round)
			.outline(cola)
		;
	}

	bool done()
	{
		return veloc.done();
	}
};

std::vector<blood_drop> blood;


struct complayer
{
	using timer = movement<support::tuple_void_t>;

	// parameters
	std::array<Program::duration, 3> punch_intervals = {600ms, 300ms, 100ms};
	Program::duration legwork_interval = 250ms;
	Program::duration stun_interval = 250ms;

	// state
	timer legwork_timer = {legwork_interval};
	timer punch_timer = {punch_intervals[0]};
	timer stun_timer = {stun_interval};
	float2 legwork_flank = {};
	int sidestep = 0;

};

struct vein
{
	std::vector<float2> path = {};
	float2 velocity = {};
	float width = 0.f;
};

std::vector<vein> bloodshot()
{
	constexpr float radius = 0.95f;
	std::vector<vein> veins;
	const auto bursts = trand_int({1, 4});
	std::generate_n(std::back_inserter(veins), bursts, [radius]()
	{
		const auto start_point = common::rotate(float2::j(radius), common::protractor<>::tau(trand_float()));
		return vein
		{
			.path = {start_point},
			.velocity = -start_point/10,
			.width = 1/30.f
		};
	});

	auto current_veins = range{support::offset_expander(veins), support::offset_expander(veins, veins.end())};
	for(int i = 3; i --> 0;)
	{
		for(vein& vein : current_veins)
		{
			for(int i = 4; i --> 0;)
			{
				const auto velocity = common::rotate(vein.velocity,
					common::protractor<>::tau(
						support::wrap(trand_float({-1/8.f, 1/8.f}), 1.f)
					)
				);

				vein.path.push_back(vein.path.back() + velocity);

				if(vein.path.back().quadrance() > radius*radius)
				{
					auto tangent = common::rotate(-vein.path.back(),
						common::protractor<>::tau(1/4.f));
					vein.path.back() -= velocity;
					vein.velocity = common::reflect(velocity, tangent);
					vein.path.back() += vein.velocity;
				}
			}
		}

		auto new_veins_end = current_veins.end();

		for(vein& v : current_veins)
		{
			auto split = 2; // = trand_int({1,3});
			const auto step = 1/2.f / split;
			auto angle_range = range{-1/4.f, -1/4.f + step};
			new_veins_end = std::generate_n(new_veins_end, split,
			[
				root = v.path.back(),
				velocity = v.velocity,
				width = v.width * 0.6f,
				&angle_range, &step
			]()
			{
				auto v = vein
				{
					.path = {root},
					.velocity = common::rotate(velocity,
						common::protractor<>::tau(
							support::wrap(trand_float(angle_range), 1.f)
						)
					),
					.width = width
				};
				angle_range += step;
				return v;
			});
		}
		current_veins.lower() = current_veins.end();
		current_veins.upper() = new_veins_end;

	}

	return veins;
}

struct eye_t
{
	constexpr static auto size = float2::one(0.035f);

	float2 position = {};
	float2 velocity = {};
	float2 drift = {};
	unsigned pop_damage = 100;
	melody<
		movement<float, quadratic_out>,
		movement<float, motion::quadratic_curve>
	>
	drifting = { {1s, 0, 1}, {1s, 1, 0}};
	melody
	<
		movement<float>,
		movement<float, quadratic_out>,
		movement<float>,
		movement<float, motion::quadratic_curve>
	>
	blink
	{
		{1s, 0.f, 0.f}, // delay
		{50ms, 0.f, 1.f}, // close
		{100ms, 1.f, 1.f}, // stay
		{50ms, 1.f, 0.f}, // open
	};
	float drift_ratio = 0.f;
	melody
	<
		movement<float>,
		movement<float, [](float x) -> float { return quadratic_out(x) * (1.f + common::sinus(x * 4.f))/2.f; } >
	>
	twitch
	{
		{1s, 0.f, 0.f}, // delay
		{500ms, 0.f, 1.f}, // close
	};
	float lid_value = 0.f;

	std::vector<vein> veins = bloodshot();

	movement<float, [](float x){ return common::sinus(x/2); }> fall = {500ms, 1.f, 2.f};
	movement<support::tuple_void_t> bleed = {10ms};

	support::array<rect, 20> black = support::make_array<20>([](auto)
	{
		return rect
		{
			size/4 * trand_float({1.f, 2.f}),
			common::protractor<>::tau(
				support::wrap(trand_float({-1/4.f, 1/4.f}), 1.f)
			),
			float2::one()/2
		};
	});
};

struct boxa
{
	rgb cola;
	float2 position = {};
	float2 velocity = {};
	float speed = 0.5;
	movement<float, common::sinus> idle = {500ms, 0.f, 0.005f};
	movement<float2, quadratic_out> shovement = {0s, float2::zero(), float2::zero()};
	melody<movement<float2>, movement<float2>> squeezement = {};
	melody<movement<float2>, movement<float2>> glove_squeezement = {};
	melody<movement<float>, movement<float, quadratic_out>> bloodbreath = {{10s, 0.f, 0.f}, {1s, 0.f, 1.f}};
	float bloodbreath_side = 1;
	boxa* target;
	std::optional<keymap> controls{};
	std::optional<complayer> brainz;

	std::array<eye_t,2> eyes = {{ {position + float2::j() * eye_t::size * 1.2f}, {position - float2::j() * eye_t::size * 1.2f} }};
	unsigned nose_damage = trand_int({50,95});

	float punch = 0.0f;
	bool hit = false;
	unsigned punch_glove = 0;
	unsigned hit_glove = 0;
	unsigned damage = 0;

	Program* program;

	auto direction() const
	{
		const auto distance = target->position - position;

		auto clamped_distance = distance;
		if(clamped_distance.y() > 0.2)
			clamped_distance.y() = 0.2;
		else if(clamped_distance.y() < -0.2)
			clamped_distance.y() = -0.2;

		if(abs(clamped_distance.x()) > 0.18f)
			clamped_distance *= float2::i();
		else
			clamped_distance = support::average(clamped_distance, clamped_distance * float2::i());

		return common::normalize(clamped_distance);
	}

	rect body() const
	{
		return {float2::one(0.1), position, half};
	};

	auto elbows() const
	{
		const auto normal = common::rotate(this->direction(), common::protractor<>::tau(1/4.f));
		auto body = this->body();
		std::array<float2,2> elbows{};
		const auto spread = 1.7f;
		auto arm = 1;
		for(auto&& elbow : elbows)
		{
			elbow = body.position;
			elbow += body.size.x() * normal * spread * arm;
			elbow += - this->direction() * body.size.x()/2 * 1.3;
			if(elbow == elbows[punch_glove])
				elbow -= body.size.x() * normal * (0.7 * punch) * arm;

			arm = -arm;
		}

		elbows[punch_glove] += this->direction() * punch * 0.1;

		return elbows;
	};

	auto gloves() const
	{
		const auto normal = common::rotate(this->direction(), common::protractor<>::tau(1/4.f));
		auto body = this->body();
		std::array<rect,2> gloves{};
		for(auto&& glove : gloves)
		{
			glove = body;
			glove.size *= 0.9;
			glove.position += body.size.x() * normal;
			glove.position += this->direction() * body.size.x()/2;
		}
		gloves[1].position -= body.size.x() * 2 * normal;
		gloves[punch_glove].position += this->direction() * punch * 0.2;

		return gloves;
	};

	void draw(frame& f, auto delta_time)
	{
		const auto direction = this->direction();
		const auto normal = common::rotate(direction, common::protractor<>::tau(1/4.f));
		const auto facingdom = float2x2{direction, normal};
		auto body = this->body();
		{
			float2 squeezement = float2::one();
			this->squeezement.move(squeezement, delta_time);
			const auto squeezed_body_size = body.size * squeezement;
			body.position += direction * (squeezed_body_size.x() - body.size.x());
			body.size = squeezed_body_size;
		}
		auto elbows = this->elbows();
		auto gloves = this->gloves();

		{
			float idle_sway = 0.f;
			auto idle_normal = normal;
			motion::loop(idle_sway, idle, delta_time);
			for(auto&& glove : gloves)
			{
				glove.position += idle_normal * idle_sway;
				idle_normal = -idle_normal;
			}
			body.position += direction * idle_sway;
		}

		{
			gloves[punch_glove].size += gloves[punch_glove].size * float2{0.1f, -0.1f} * punch;
		}

		{
			float2 squeezement = float2::one();
			this->glove_squeezement.move(squeezement, delta_time);
			const auto squeezed_glove_size = gloves[hit_glove].size * squeezement;
			gloves[hit_glove].position += direction * (squeezed_glove_size.x() - gloves[hit_glove].size.x());
			gloves[hit_glove].size = squeezed_glove_size;

		}

		{
			auto scaled_body = scale(body,ring);
			for(auto&& [elbow, glove] : range{vbegin(elbows, gloves), vend(elbows,gloves)})
			{
				auto scaled_elbow = elbow * ring.size + ring.position;
				auto scaled_glove = scale(glove,ring);
				f.begin_sketch()
					.move(scaled_body.position)
					.bezier(scaled_elbow, scaled_glove.position)
					.line_width((0.04f) * ring.size.x()).outline(0x333333_rgb)
				;
			}

			f.begin_sketch()
				.ellipse(scaled_body.position, facingdom * geom::column(scaled_body.size/2))
				.fill(rgb::white(0.5))
			;
		}

		{
			auto thumb_offsetdom = geom::vector(
				-direction * 0.1f,
				-normal * 0.4f
			);
			auto glove_sketch = f.begin_sketch();
			for(auto&& glove : gloves)
			{
				auto scaled_glove = scale(glove,ring);
				glove_sketch
					.ellipse(scaled_glove.position, facingdom * geom::column(scaled_glove.size/2))
					.ellipse(scaled_glove.position + thumb_offsetdom(scaled_glove.size), facingdom * geom::column(scaled_glove.size/2/2))
				;
				thumb_offsetdom[1] = -thumb_offsetdom[1];
			}
			glove_sketch.fill(cola);
		}

		// update nose
		if(damage > nose_damage)
		{
			float bubble_scale = 0.f;
			auto looped = motion::loop(bubble_scale, bloodbreath, delta_time);

			const auto bubble = rect{body.size/4 * bubble_scale, body.position + body.size/2 * direction * 1.1f};
			const auto scaled_bubble = scale(bubble, ring);
			f.begin_sketch()
				.ellipse(scaled_bubble.position + bloodbreath_side * normal * scaled_bubble.size * 0.5f, facingdom * geom::column(scaled_bubble.size/2))
				.fill(rgba::red(0.6f, 0.6f))
			;

			if(looped)
			{
				for(int i = 3; i --> 0;) blood.push_back({
					rgb::red(0.6f),
					{250ms, float2x2{direction, bloodbreath_side*normal}(trand_float2() / 100), float2::zero()},
					bubble.position,
					0.005f
				});

				auto bobble = [](auto ratio) mutable
				{ return std::pair{ratio < 0.5f ? way(25.f, 0.f, ratio*2) : way(0.f, 25.f, (ratio - 0.5f)*2), 1.f}; };
				program->request_wave({faded_wave(20, std::move(bobble)), 125ms});

				bloodbreath.total<0>() = 5s + trand_float() * 10s;
				bloodbreath.total<1>() = 500ms + trand_float() * 1s;
				bloodbreath_side = trand_int({0,2}) == 0 ? -1 : 1;
			}
		}

		{
			const auto scaled_nose = (damage < nose_damage)
				? scale(rect{body.size/4 + float2::i() * body.size/4, body.position + body.size/2 * 0.8f * direction}, ring)
				: scale(rect{body.size/4 - float2::i() * body.size/4/2, body.position + body.size/2 * direction}, ring)
			;

			auto nose_sketch = f.begin_sketch();
			nose_sketch.ellipse(scaled_nose.position, facingdom * geom::column(scaled_nose.size/2));
			nose_sketch.fill(rgb::white(0.5));
		}

		auto socket_position = [&](float side) { return body.position + eye_t::size/2 * (direction + side * normal * 1.2f); };

		{ // move eyes, techincally physics but purely visual and need the visually augmented body geometry
			auto eye_difference = eyes[0].position - eyes[1].position;
			const auto overlapance = eye_t::size.x() * eye_t::size.x() - eye_difference.quadrance();

			for(auto&& eye : eyes)
			if(damage < (eye.pop_damage + 20) && motion::loop(eye.drift_ratio, eye.drifting, delta_time))
			{
				eye.drift = common::rotate(float2::i(trand_float({0.2f, 0.8f})), common::protractor<>::tau(trand_float()));
				eye.drifting.total<0>() = eye.drifting.total<1>() = 700ms + trand_float() * 200ms;
			}

			if(overlapance > 0)
			{
				auto impact_surface = common::rotate(eye_difference, common::protractor<>::tau(1/4.f));
				float side = 1;
				for(auto&& eye : eyes)
				{
					eye.drift = common::reflect(eye.drift, impact_surface);
					eye.position += side * common::normalize(eye_difference) * support::root2(overlapance)/2;
					side = -side;
				}
			}

			float side = 1;
			for(auto&& eye : eyes)
			{
				if(damage >= (eye.pop_damage + 20) && not eye.fall.done())
				{
					if(eye.fall.advance(delta_time))
					{
						for(int i = 3; i --> 0;) blood.push_back({
							0xffbbbb_rgb,
							{250ms, (trand_float2() - 0.5f) / 50, float2::zero()},
							eye.position,
							0.01f
						});
						for(int i = 20; i --> 0;) blood.push_back({
							rgb::red(0.8f),
							{250ms, (trand_float2() - 0.5f) / 50, float2::zero()},
							eye.position,
							0.005f
						});
						blood.push_back({
							rgb::white(0), // hmm...
							{250ms, (trand_float2() - 0.5f) / 50, float2::zero()},
							eye.position,
							0.005f
						});

						auto spit = [noise = 1.f](auto ratio) mutable
						{
							if(unsigned(480*5 * ratio) % 10 == 0)
								noise = trand_float({ratio/2, 0.5f + (1-ratio)/2});;
							return std::pair
							{
								common::cosinus(ratio) * 20.f + 20.f,
								(1 - ratio) * noise
							};
						};
						program->request_wave({faded_wave(20, std::move(spit)), 50ms});

					}

					if(motion::loop(eye.bleed, delta_time))
					{
						const auto velocity = common::rotate(direction,
							common::protractor<>::tau(
								support::wrap(trand_float({-1/16.f, 1/16.f}), 1.f)
							)
						);
						for(int i = 3; i --> 0;) blood.push_back({
							rgb::red(0.8f),
							{250ms, velocity / 50 * eye.fall.value()/2, float2::zero()},
							socket_position(side),
							0.005f
						});
					}
				}
				auto eye_offset = socket_position(side) - eye.position;
				eye.velocity = (damage < (eye.pop_damage + 20)) ? 10 * eye_offset : (eye.drift * (1-eye.drift_ratio));
				eye.position += (eye.velocity + eye.drift_ratio * eye.drift) * delta_time.count();
				if(damage < eye.pop_damage)
					eye.position = socket_position(side);

				side = -side;
			}
		}

		{
			float side = 1;
			for(auto&& eye : eyes)
			{
				if(damage > eye.pop_damage/2)
				{
					for(auto blackeye : eye.black)
					{
						blackeye.position = socket_position(side) + common::rotate(direction * eye_t::size/2 * 0.9f, blackeye.position);
						f.begin_sketch()
							.ellipse(scale(blackeye, ring))
							.fill(0x55007733_rgba)
						;
					}
				}
				auto scaled_socket = scale(rect{eye_t::size * 0.8, socket_position(side)}, ring);
				f.begin_sketch()
					.ellipse(scaled_socket.position, facingdom * geom::column(scaled_socket.size/2))
					.fill(0x660000_rgb)
				;
				side = -side;
			}
		}

		for(auto&& eye : eyes)
		{
			if(damage < eye.pop_damage * 0.8f)
			{
				if(0 != motion::loop(eye.lid_value, eye.blink, delta_time))
				{
					eye.blink.total<0>() = 500ms + trand_float() * 2s;
					float heft = std::max((damage - eye.pop_damage/3.f)/(eye.pop_damage/3.f), 0.f);
					eye.blink.total<2>() = eye.blink.total<2>() = 100ms + heft * 500ms;
				}
			}
			else
			{
				if(0 != motion::loop(eye.lid_value, eye.twitch, delta_time))
				{
					eye.twitch.total<0>() = 500ms + trand_float() * 2s;
				}
			}
		}
		if(damage < std::min(eyes[0].pop_damage, eyes[1].pop_damage)/3)
		{
			eyes[0].blink.total<0>() = eyes[1].blink.total<0>();
		}

		{ // put the twitching eyelid under the popped eye
			float side = 1;
			for(auto&& eye : eyes)
			{
				if(damage >= eye.pop_damage)
				{
					auto scaled_eyelid = scale(rect{eye_t::size * 0.8f, socket_position(side)}, ring);
					auto center_offset = direction * (0.4f + eye.lid_value) * scaled_eyelid.size.x()/2;
					f.begin_sketch()
						.arc(scaled_eyelid.position, range{normal, -normal} * scaled_eyelid.size.x()/2, 3)
						.sector(scaled_eyelid.position + center_offset, range{-normal, normal} * scaled_eyelid.size.x()/2 - center_offset, -3)
						.fill(rgb::red(0.8f))
					;
				}
				side = -side;
			}
		}

		{
			float side = 1;
			for(auto&& eye : eyes)
			{
				auto scaled_socket = scale(rect{eye_t::size * 0.8, socket_position(side)}, ring);
				auto scaled_eye = scale(rect{eye_t::size, eye.position}, ring);
				auto distance = (socket_position(side) - eye.position).quadrance();

				auto scaled_root = scale(rect{eye_t::size * 0.4, socket_position(side)}, ring);
				f.begin_sketch()
					.ellipse(scaled_root.position, facingdom * geom::column(scaled_root.size/2))
					.fill(rgb::red(0.8f))
				;
				if(damage < (eye.pop_damage + 20))
					f.begin_sketch()
						.line(scaled_socket.position, scaled_eye.position)
						.line_width( scaled_socket.size.x() * std::max(0.2f, 1.f-distance/(eye_t::size.x()*eye_t::size.x()*9))/2 )
						.outline(rgb::red(0.8f))
					;
				side = -side;
			}
		}

		const auto look_direction = common::normalize(target->position - position);
		for(auto&& eye : eyes)
		{
			bool eye_popped = damage >= eye.pop_damage;
			const auto eye_direction = (eye.velocity != float2::zero() && eye_popped) ? -common::normalize(eye.velocity) : look_direction;
			const auto eye_normal = common::rotate(eye_direction, common::protractor<>::tau(1/4.f));
			const auto eye_stretch = eye_popped ? 1+eye.velocity.quadrance() : 1;
			const auto eye_facingdom = float2x2{eye_direction * eye_stretch, eye_normal};
			auto scaled_eye = scale(rect{eye_t::size, eye.position}, ring);
			const auto pupil_size = eye_t::size * 0.45f * eye.fall.value();
			// WISH: make pupil a proper crescent and go up the eye when blinked
			const auto pupil_blink_offset = (damage < eye.pop_damage * 0.8f) ? eye_direction * eye.lid_value * pupil_size/4 : float2::zero();
			auto scaled_pupil = scale(rect{pupil_size, eye.position + eye_direction * pupil_size - pupil_blink_offset}, ring);
			if(not eye.fall.done())
			{
				f.begin_sketch()
					.ellipse(scaled_pupil.position, eye_facingdom * geom::column(scaled_pupil.size/2))
					.fill(rgb::white(0.f)) // hmm...
				;
				f.begin_sketch()
					.ellipse(scaled_eye.position, eye_facingdom * geom::column(scaled_eye.size/2 * eye.fall.value())) // TODO: specify vertex count here and elsewhere
					.fill(wayback(0xffffff_rgb, 0xffbbbb_rgb, std::min(1.f, damage/(eye.pop_damage * 0.8f))))
				;
			}

			// TODO: graduate by bursts and animate em filling up
			if(damage > eye.pop_damage * 0.6 && not eye.fall.done())
			for(auto&& vein : eye.veins) // this eats up a lot of cpu :( i blame nanovg and C in general who just can't libraries >:(
			{
				auto sketch = f.begin_sketch();
				sketch.move(eye_facingdom(vein.path.front()) * scaled_eye.size/2 + scaled_eye.position);
				for(auto&& x : range{vein.path.begin()+1, vein.path.end()})
				{
					sketch.vertex(eye_facingdom(x) * scaled_eye.size/2 * eye.fall.value() + scaled_eye.position);
				}
				sketch
					.line_width(vein.width * scaled_eye.size.x() * eye.fall.value())
					.line_cap(sketch::cap::round)
					.line_join(sketch::join::round)
					.outline(rgb::red())
				;
			}
		}

		{
			float side = 1;
			for(auto&& eye : eyes)
			{
				if(damage < eye.pop_damage * 0.8f)
				{
					auto scaled_eyelid = scale(rect{eye_t::size * 1.2, socket_position(side)}, ring);
					auto center_offset = direction * (1-eye.lid_value) * scaled_eyelid.size.x();
					auto color = damage > eye.pop_damage/2 ? rgb(0x554477_rgb) : rgb::white(0.4f);
					f.begin_sketch()
						.arc(scaled_eyelid.position, range{normal, -normal} * scaled_eyelid.size.x()/2, 3) // backlid
						// .sector(scaled_eyelid.position - center_offset, (range{-normal, normal} * scaled_eyelid.size.x()/2) + center_offset, 3) // wtf gcc 11.4.0
						.sector(scaled_eyelid.position - center_offset, range{-normal, normal} * scaled_eyelid.size.x()/2 + center_offset, 3) // frontlid
						.fill(color)
					;
				}
				else if(damage < eye.pop_damage)
				{
					auto scaled_eyelid = scale(rect{eye_t::size, socket_position(side)}, ring);
					scaled_eyelid.size += 1; // freakin antialiasing
					auto center_offset = direction * (0.4f + eye.lid_value) * (scaled_eyelid.size.x()/2);
					f.begin_sketch()
						.arc(scaled_eyelid.position, range{normal, -normal} * scaled_eyelid.size.x()/2, 3)
						.sector(scaled_eyelid.position + center_offset, range{-normal, normal} * scaled_eyelid.size.x()/2 - center_offset, -3)
						.fill(rgb::red(0.8f))
					;
				}
				side = -side;
			}
		}
	}

	void move(auto delta_time)
	{
		const range2f bounds {position - 0.05, position + 0.05};
		constexpr auto world_bounds = range2f{float2::zero(), float2::one()};
		const auto lower_overshoot = world_bounds.lower() - bounds.lower();
		const auto upper_overshoot = bounds.upper() - world_bounds.upper();

		constexpr float springback_factor = 20;
		velocity += springback_factor * max(lower_overshoot, float2::zero());
		velocity -= springback_factor * max(upper_overshoot, float2::zero());

		position += velocity * speed * delta_time.count();
		if(not shovement.done())
		{
			position += shovement.value() * (speed /2) * delta_time.count();
			shovement.advance(delta_time);
		}

		auto diameter = 0.2f;
		const auto distance = target->position - position;
		if(distance.quadrance() < diameter * diameter)
		{
			auto direction = common::normalize(distance);
			auto force_field = direction * diameter;
			auto correction = distance - force_field;
			position += correction;
		}

		if(punch > 0 || hit)
		{
			if(not hit)
			{
				punch += 10 * delta_time.count();;

				if(punch >= 1)
				{
					hit = true;
					punch = 1;
				}
			}
			else
			{
				if(punch > 0)
				{
					punch -= 10 * delta_time.count();
				}
				else
				{
					punch = 0;
					hit = false;
				}
			}
		}
	}

	void update(frame& f, auto delta_time)
	{
		move(delta_time);
		draw(f, delta_time);
	}

	std::optional<rect> punching_glove() const
	{
		const bool forward = punch > 0 && not hit;
		const bool edge_case = punch >= 1; // same frame as hit set to true but still want collision checks
		if(forward || edge_case)
			return gloves()[punch_glove];
		else
			return std::nullopt;
	}

	unsigned closest_glove_index() const
	{
		return
			(position.y() > target->position.y())
			^
			(position.x() > target->position.x())
		;
	}

	rect closest_glove() const
	{ return gloves()[closest_glove_index()]; }

};

std::array boxas
{
	boxa{rgb::red(), starting_pos},
	boxa{rgb::blue(), float2::one() - starting_pos}
};

constexpr rect fit_square(float2 size)
{
	float2 fit_size = float2::one(*min_element(size));
	return {fit_size, (size - fit_size) / 2}; // hmmm, does this sub/2 thing have a name? midpointish but not quite
}

void punch(Program& program, boxa& boxa)
{
	if(boxa.punch == 0)
	{
		auto hiss = [rand = make_trand_float({0,0.1})](auto ratio) mutable
		{ return std::pair{rand(), ratio/5}; };
		program.request_wave({faded_wave(20, std::move(hiss)), 125ms});
		// program.request_wave({faded_wave(20, std::move(hiss)), 50ms});

		boxa.punch = 0.001;
		boxa.punch_glove = boxa.closest_glove_index();
	}
}

void start(Program& program)
{
	// program.frametime = framerate<30>::frametime;
	// program.frametime = framerate<60>::frametime;
	// program.frametime = framerate<120>::frametime;
	program.resizable = true;
	program.size = float2::one(400.f);
	ring = fit_square(program.size);

	boxas[0].target = &boxas[1];
	boxas[1].target = &boxas[0];
	boxas[1].brainz = complayer{};
	// boxas[0].brainz = complayer{};

	for(auto&& boxa : boxas)
	{
		boxa.idle.advance(trand_float() * boxa.idle.total);
		auto last_pop = trand_int({70,91});
		auto last_eye = trand_int({0,2});
		boxa.eyes[last_eye].pop_damage = last_pop;
		boxa.eyes[not last_eye].pop_damage = last_pop - trand_int({10,21});
		boxa.program = &program;
	}

	if(program.argc > 2)
	{
		using support::ston;
		using seed_t = decltype(tiny_rand());
		tiny_rand.seed({ ston<seed_t>(program.argv[1]), ston<seed_t>(program.argv[2]) });
	}

	std::cout << "seed: " << std::hex << std::showbase << tiny_rand << '\n';

	program.key_up = [&](scancode code, keycode)
	{
		switch(code)
		{
			case scancode::leftbracket:
			case scancode::c:
				if(pressed(scancode::rctrl) || pressed(scancode::lctrl))
			case scancode::escape:
				program.end();
			break;

			default: break;
		}
	};

	program.key_down = [&](scancode code, keycode)
	{
		for(auto&& boxa : boxas)
			if(boxa.controls)
				if(support::find(boxa.controls->punch, code) != boxa.controls->punch.end())
					punch(program, boxa);
	};

	program.mouse_down = [&](float2, auto)
	{
	};

	program.size_changed = [&](float2 size)
	{
		std::cout << size << '\n';
		ring = fit_square(size);
		std::cout << ring << '\n';
	};

	program.draw_loop = [&](auto frame, auto delta_time)
	{
		frame.begin_sketch()
			.rectangle(rect{frame.size})
			.fill(rgb::white(0.4f))
		;

		frame.begin_sketch()
			.rectangle(ring)
			.fill(rgb(0.3f, 0.4f, 0.0f))
		;

		for(auto&& boxa : boxas)
		{
			boxa.velocity = float2::zero();
			if(boxa.controls)
			{
				for(auto&& kv : boxa.controls->legwork)
					if(pressed(kv.k))
						boxa.velocity += kv.v;
				if(boxa.velocity != float2::zero())
					boxa.velocity = common::normalize(boxa.velocity); // here length can only be root(2) or 1 so a bit of an overkill
			}
			else
			{
				auto selected = support::find_if(control_options, [](auto option)
				{
					return support::any_of(option.legwork, [](auto kv) { return pressed(kv.k); });
				});

				if(selected != control_options.end())
				{
					boxa.controls = *selected;
					control_options.erase(selected);
				}
			}

			if(boxa.brainz)
			{
				if(boxa.brainz->stun_timer.done())
				{
					// FIXME: need more randomness for comp v comp demo
					const auto get_flank_offset = [&boxa](float2 flank){ return boxa.target->position + flank - boxa.position; };
					if(motion::loop(boxa.brainz->legwork_timer, delta_time) != 0)
					{
						const auto direction = boxa.target->direction();
						const auto normal = common::rotate(direction, common::protractor<>::tau(1/4.f));
						const auto faceoff = direction * boxa.target->body().size * 2.9;
						const auto backstab = -faceoff;
						const auto normal_flank = faceoff + normal * boxa.target->body().size;
						const auto unnormal_flank = faceoff - normal * boxa.target->body().size;
						const auto back_normal_flank = backstab + normal * boxa.target->body().size;
						const auto back_unnormal_flank = backstab - normal * boxa.target->body().size;
						std::array<float2, 4> candidates {normal_flank, unnormal_flank, back_normal_flank, back_unnormal_flank};
						auto ring_candidates_end = std::remove_if(candidates.begin(), candidates.end(),
							[&boxa](auto flank){ return not range{float2::zero(),float2::one()}.contains(boxa.target->position + flank); });
						using support::transform_arg;
						boxa.brainz->legwork_flank = *std::min_element(candidates.begin(), ring_candidates_end,
							transform_arg{get_flank_offset, transform_arg{&float2::quadrance}});

						if(boxa.direction().y() != 0 && (boxa.position - boxa.target->position).quadrance() < 0.04)
						{
							auto normal = common::rotate(boxa.direction(), common::protractor<>::tau(1/4.f));
							auto diff = boxa.target->position - boxa.position;
							boxa.brainz->sidestep = normal(diff) > 0 ? -1 : 1;
						}
						else
							boxa.brainz->sidestep = 0;
					}

					auto flank_offset = get_flank_offset(boxa.brainz->legwork_flank);
					if(flank_offset.quadrance() > 0.0001)
						boxa.velocity = common::normalize(flank_offset);

					if(boxa.brainz->sidestep != 0)
					{
						auto normal = common::rotate(boxa.direction(), common::protractor<>::tau(1/4.f));
						boxa.velocity = normal * boxa.brainz->sidestep;
					}

					{
						const auto closest_glove_offset = (boxa.target->position - boxa.closest_glove().position);
						if(boxa.brainz->punch_timer.done())
						{
							// make these complayer parameters, along with faceoff distance above close
							const float punch_length = 0.26f;
							const float punch_width = 0.25f * (0.1f + 0.9f * std::max((1.f - boxa.damage/100.f),0.f));
							auto normal = common::rotate(boxa.direction(), common::protractor<>::tau(1/4.f));
							if
							(
								closest_glove_offset(boxa.direction()) < punch_length
								&& closest_glove_offset(normal) < punch_width
								&& closest_glove_offset(-normal) < punch_width
							)
							{
								punch(program, boxa);
								boxa.brainz->punch_timer.total = boxa.brainz->punch_intervals[trand_int({0, (int)boxa.brainz->punch_intervals.size()})];
								boxa.brainz->punch_timer.total *= 0.25f + 0.75f * std::max((1.f - boxa.damage/100.f),0.f);
								boxa.brainz->punch_timer.reset();
							}
						}
						else
							boxa.brainz->punch_timer.advance(delta_time);
					}

					// frame.begin_sketch().rectangle(scale(rect{{float2::one(0.1), boxa.position + flank_offset}, float2::one(0.5f)},ring)).fill(0x00000033_rgba);
					// frame.begin_sketch().rectangle(scale(rect{{float2::one(0.1), boxa.gloves()[boxa.punch_glove].position}, float2::one(0.5f)},ring)).fill(0x00000033_rgba);
				}
				else
					boxa.brainz->stun_timer.advance(delta_time);
			}
		}

		for(auto&& b : boxas) b.update(frame, delta_time);
		for(auto&& b : boxas) // collision checks
		{
			auto glove = b.punching_glove();
			bool collided_target = false;
			bool collided_a_glove = false;
			bool collided_punching_glove = false;

			if(glove)
			{
				collided_target = (glove->position - b.target->position).quadrance() <= 0.085*0.085;

				if(not collided_target)
				{
					auto other_gloves = b.target->gloves();

					auto collided_glove = support::find_if(other_gloves, [pos = glove->position](auto x) {
						return (pos - x.position).quadrance() <= x.size.quadrance()/2;
					});
					collided_a_glove = collided_glove != other_gloves.end();
					auto other_punching_glove = b.target->punching_glove();
					if(collided_a_glove && other_punching_glove)
					{
						collided_punching_glove = (*collided_glove) == (*other_punching_glove);
					}
					if(collided_a_glove)
					{
						b.target->hit_glove = collided_glove - other_gloves.begin();
					}
				}
			}


			if(collided_target)
			{
				b.target->shovement.total = 250ms;
				b.target->bloodbreath.reset();

				const auto normal = common::rotate(b.direction(), common::protractor<>::tau(1/4.f));
				const auto target_normal = common::rotate(b.target->direction(), common::protractor<>::tau(1/4.f));
				const auto target_offset = b.position - b.target->position;
				b.target->shovement.start = (b.direction() + (target_normal(target_offset) < 0 ? normal*2 : -normal*2)) * 5;

				b.target->shovement.reset();
				{
					constexpr auto squeezementat = float2{0.5f, 1.5f};
					b.target->squeezement = melody{
						movement<float2>{125ms, float2::one(), squeezementat},
						movement<float2>{125ms, squeezementat, float2::one()}
					};
				}
				++b.target->damage; // TODO: more damage for combos?

				bool skip_boink = false;
				for(auto&& eye : b.target->eyes)
				{
					if(b.target->damage == eye.pop_damage)
					{
						auto plok = [](auto ratio)
						{ return std::pair{common::cosinus(ratio) * 10.f + 10.f, 1}; };
						program.request_wave({faded_wave(20, std::move(plok)), 50ms});
						skip_boink = true;
					}

					if(b.target->damage == eye.pop_damage + 20)
					{
						auto tips = [noise = 1.f](auto ratio) mutable
						{
							ratio = 1-ratio;
							if(unsigned(480*5 * ratio) % 10 == 0)
								noise = trand_float({ratio/2, 0.5f + (1-ratio)/2});;
							return std::pair
							{
								common::cosinus(ratio) * 20.f + 20.f,
								(1 - ratio) * noise
							};
						};
						program.request_wave({faded_wave(20, std::move(tips)), 50ms});
						skip_boink = true;
					}
				}

				if(b.target->damage == b.target->nose_damage)
				{
					auto crash = [noise = 1.f](auto ratio) mutable
					{
						if(unsigned(4800 * ratio) % 10 == 0)
							noise = trand_float();;
						return std::pair
						{
							common::cosinus(ratio) * 4.f + 4.f,
							(1 - ratio) * noise
						};
					};
					program.request_wave({faded_wave(20, std::move(crash)), 250ms});
					// skip_boink = true;
				}

				if(not skip_boink)
				{
					auto boink = [](auto ratio)
					{ return std::pair{way(40.f, 20.f, ratio), 1}; };
					program.request_wave({faded_wave(20, std::move(boink)), 125ms});
				}

				const auto splatter_direction = (b.target->position - b.position).signum();
				for(int i = 10; i --> 0;) blood.push_back({
					wayback(0x55cccc_rgb,0x660000_rgb, std::min(b.target->damage/50.f, 1.f)),
					{250ms, trand_float2() / 50 * splatter_direction, float2::zero()},
					b.target->position
				});

				if(b.target->brainz)
				{
					b.target->brainz->stun_timer.total = b.target->brainz->stun_interval * (0.10f + 0.90f * std::max((1.f - b.target->damage/100.f),0.f));
					b.target->brainz->stun_timer.reset();
				}

				std::cout << std::dec << boxas[0].damage << " vs " << boxas[1].damage << '\n';
			}

			if(collided_a_glove)
			{
				constexpr auto squeezementat = float2{0.7f, 1.3f};
				b.target->glove_squeezement = melody{
					movement<float2>{60ms, float2::one(), squeezementat},
					movement<float2>{60ms, squeezementat, float2::one()}
				};

				auto bunk = [](auto ratio)
				{ return std::pair{way(25.f, 0.f, ratio), .3f}; };
				program.request_wave({faded_wave(20, std::move(bunk)), 125ms});
			}

			if(collided_target || collided_a_glove)
				b.hit = true;

			if(collided_punching_glove)
			{
				b.target->hit = true;
				break;
			}
		}

		for(auto&& b : blood) b.update(frame, delta_time);
		blood.erase(std::remove_if(blood.begin(),blood.end(), [](auto& b) {return b.done();}), blood.end());

	};
















	// program.set_volume_levels(support::filled_array<32>(0.25f));
	// musac

	program.request_wave({
		[
			time = 0.f, freq = 0.f,
			musac = motion::melody
			{
				movement<float>{50ms, 0.f,120.f},
				movement<float>{50ms, 120.f,0.f},
				movement<float>{100ms, 0.f,0.f},
				movement<float>{50ms, 0.f,150.f},
				movement<float>{50ms, 150.f,0.f},
				movement<float>{100ms, 0.f,0.f},
				movement<float>{50ms, 0.f,100.f},
				movement<float>{50ms, 100.f,0.f},

				movement<float>{100ms, 0.f,0.f},
				movement<float>{50ms, 0.f,200.f},
				movement<float>{50ms, 200.f,0.f},
				movement<float>{100ms, 0.f,0.f},

				movement<float>{50ms, 0.f,220.f},
				movement<float>{50ms, 220.f,0.f},
				movement<float>{100ms, 0.f,0.f},
				movement<float>{50ms, 0.f,250.f},
				movement<float>{50ms, 250.f,0.f},
				movement<float>{100ms, 0.f,0.f},
				movement<float>{50ms, 0.f,200.f},
				movement<float>{50ms, 200.f,0.f},

				movement<float>{100ms, 0.f,0.f},
				movement<float>{50ms, 0.f,100.f},
				movement<float>{50ms, 100.f,0.f},
				movement<float>{100ms, 0.f,0.f},



				movement<float>{50ms, 0.f,120.f},
				movement<float>{50ms, 120.f,0.f},
				movement<float>{100ms, 0.f,0.f},
				movement<float>{50ms, 0.f,0.f},
				movement<float>{50ms, 0.f,0.f},
				movement<float>{100ms, 0.f,0.f},
				movement<float>{50ms, 0.f,100.f},
				movement<float>{50ms, 100.f,0.f},

				movement<float>{100ms, 0.f,0.f},
				movement<float>{50ms, 0.f,200.f},
				movement<float>{50ms, 200.f,0.f},
				movement<float>{100ms, 0.f,0.f},

				movement<float>{50ms, 0.f,0.f},
				movement<float>{50ms, 0.f,0.f},
				movement<float>{100ms, 0.f,0.f},
				movement<float>{50ms, 0.f,250.f},
				movement<float>{50ms, 250.f,0.f},
				movement<float>{100ms, 0.f,0.f},
				movement<float>{50ms, 0.f,200.f},
				movement<float>{50ms, 200.f,0.f},

				movement<float>{100ms, 0.f,0.f},
				movement<float>{50ms, 0.f,0.f},
				movement<float>{50ms, 0.f,0.f},
				movement<float>{100ms, 0.f,0.f},
			}
		](auto tick) mutable
		{
			motion::loop(freq, musac, Program::duration(tick));
			time += tick * freq;
			time = support::wrap(time,1.f);
			return stairwave(time) * 0.2f;
		},
	0ms});

	program.request_wave({
		[
			time = 0.f, freq = 0.f,
			musac = motion::melody
			{

				movement<float, quadratic_out>{10ms, 0.f, 350.f},
				movement<float, quadratic_out>{90ms, 350.f,420.f},
				movement<float, motion::quadratic_curve>{200ms, 420.f,350.f},
				movement<float, quadratic_out>{100ms, 350.f,350.f},
				movement<float, quadratic_out>{100ms, 350.f,750.f},
				movement<float, motion::quadratic_curve>{200ms, 750.f,350.f},
				movement<float, quadratic_out>{100ms, 350.f,350.f},

				movement<float, motion::quadratic_curve>{200ms, 350.f,400.f},
				movement<float, quadratic_out>{100ms, 400.f,350.f},
				movement<float, quadratic_out>{500ms, 350.f,350.f},

				movement<float, quadratic_out>{100ms, 350.f,420.f},
				movement<float, motion::quadratic_curve>{200ms, 420.f,350.f},
				movement<float, quadratic_out>{100ms, 350.f,350.f},
				movement<float, quadratic_out>{100ms, 350.f,750.f},
				movement<float, motion::quadratic_curve>{200ms, 750.f,350.f},
				movement<float, quadratic_out>{90ms, 350.f,350.f},
				movement<float, quadratic_out>{10ms, 350.f,0.f},

				movement<float>{800ms, 0.f,0.f},

				movement<float>{800ms, 0.f,0.f},
				movement<float>{800ms, 0.f,0.f},
				movement<float>{800ms, 0.f,0.f},
				movement<float>{800ms, 0.f,0.f},

			}
		](auto tick) mutable
		{
			motion::loop(freq, musac, Program::duration(tick));
			time += tick * (freq/3);
			time = support::wrap(time,1.f);
			return stairwave(time) * 0.3f;
		},
	0ms});

	program.request_wave({
		[
			time = 0.f, freq = 0.f,
			musac = motion::melody
			{

				movement<float>{800ms, 0.f,0.f},
				movement<float>{800ms, 0.f,0.f},
				movement<float>{800ms, 0.f,0.f},
				movement<float>{800ms, 0.f,0.f},

				movement<float>{800ms, 0.f,0.f},
				movement<float>{800ms, 0.f,0.f},
				movement<float>{800ms, 0.f,0.f},
				movement<float>{800ms, 0.f,0.f},

				movement<float>{800ms, 0.f,0.f},
				movement<float>{800ms, 0.f,0.f},
				movement<float>{800ms, 0.f,0.f},
				movement<float>{800ms, 0.f,0.f},

				movement<float, quadratic_out>{400ms, 0.f, 1350.f},
				movement<float, quadratic_out>{400ms, 1350.f,1000.f},
				movement<float, quadratic_out>{400ms, 1000.f, 1200.f},
				movement<float, quadratic_out>{400ms, 1200.f, 350.f},
				movement<float, quadratic_out>{400ms, 350.f,1000.f},
				movement<float, quadratic_out>{400ms, 1000.f, 0.f},

				movement<float>{800ms, 0.f,0.f},


			}
		](auto tick) mutable
		{
			motion::loop(freq, musac, Program::duration(tick));
			time += tick * (freq/3);
			time = support::wrap(time,1.f);
			return stairwave(time) * 0.1f;
		},
	0ms});

}


// more than 1000 lines for this? sad...
